# 1. cn.bing.com搜索引擎 歌曲+site:www.gequbao.com 列表第一个结果优先
# https://cn.bing.com/search?q={name}+site:music.163.com
# 2. 重定向上述url,响应头location的url,下载歌曲并重命名歌曲名
# http://m10.music.126.net/20230203132034/5fc3556fc1ae3e6f1f58d2cdaee73687/ymusic/obj/w5zDlMODwrDDiGjCn8Ky/14056611276/4189/2f23/d8bd/8d8ea863b76428203e71d7b35db172ae.mp3
# 歌词
# http://music.163.com/api/song/media?id=1903991886
import time
import eyed3
from lxml import etree
import requests
from tqdm import tqdm
from functools import partial
import os
import json
import re
from collections import OrderedDict


class MusicDownloader:
    def __init__(self, songs_dirpath="musicdownloader", proxy_state=False):
        self.songs_dirpath = songs_dirpath
        self.proxy_state = proxy_state

        # 请求cn.bing.com session
        self.bing = requests.Session()
        # 请求music.163.com session
        self.music163 = requests.Session()
        # 请求p1.music.126.net session
        self.p1 = requests.Session()

        # 进度条设置
        self.pbar = tqdm(desc="批量歌曲下载进度")

    def proxy(self, url: str):
        proxy_url = "http://10.10.10.5:5010"

        proxy = ""
        while url.split(":")[0] not in proxy.split(":")[0]:
            proxy = requests.get(proxy_url+"/get").json()["proxy"]

        return {"http": 'http://' + proxy, "https": 'https://' + proxy} if proxy.startswith("https") else {"http": 'http://' + proxy}

    def get(self, func, url: str):
        header = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
            'Accept-Encoding': 'gzip, deflate,br', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
            'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.70"}
        proxys = self.proxy() if self.proxy_state else None
        try:
            result = func(url, header, proxys)
        except Exception as e:
            print(f"get {func} Error:{e}")
            return False
        else:
            return result

    def request_bing(self, url: str, headers: dict, proxies: dict or None):
        resp = self.bing.get(url, headers=headers, proxies=proxies)
        if resp.status_code == requests.codes.ok:
            hl = etree.HTML(resp.text)
            if hl is not None:
                for li in hl.xpath('//li'):
                    u = str(li.xpath())
                    if u.startswith('https://music.163.com/song') or u.startswith('http://music.163.com/m/song'):
                        return u
                # print(f"\nurls: {urls}")
                return False
            else:
                return False
        else:
            return None

    def download_mp3(self, url: str, headers: dict, proxies: dict or None, path: str):
        resp = self.music163.get(url, headers=headers,
                                 proxies=proxies, stream=True)
        try:
            with open(path, "wb") as f:
                for chunk in resp.iter_content(chunk_size=128):
                    f.write(chunk)
        except OSError as e:
            print(f"\ndownload mp3 Error: {e}")
            return False
        else:
            return True

    def download_lyric(self, url: str, headers: dict, proxies: dict or None):
        resp = self.music163.get(url, headers=headers, proxies=proxies)
        if resp.status_code == 200:
            lyric = json.loads(resp.text)["lyric"]

            return lyric
        else:
            return False

    def get_image(self, url, headers: dict, proxies: dict or None,):
        resp = self.p1.get(url, headers=headers, proxies=proxies)
        if resp.status_code == 200:
            # 封面图片字节流
            return resp.content
        else:
            return False

    def write_ID3_2(self, path: str, img: bytes, title="", artist="", album="", genre="", lyric=""):
        # print(
        #     f"\npath:{path}\nimg:{len(img)}\ntitle:{title}\nartist:{artist}\nalbuum:{album}\ngenre:{genre}\nlyric:{repr(lyric)}")
        audio = eyed3.load(path)
        if audio is None:
            return
        if not audio.tag:
            audio.initTag(version=(2, 3, 0))

        tag = audio.tag

        # song.tag.artist = "Behemoth"
        # song.tag.album = "The Satanist"
        # song.tag.album_artist = "Various Artists"
        # song.tag.genre = "Black Metal"
        # song.tag.recording_date = 2014
        # song.tag.track_num = 4
        # song.tag.title = "Ora pro nobis Lucifer"
        # song.tag.lyrics.set(u"[04:35.488]爱人就错过")
        # song.tag.lyrics[0].text

        # with open(cover_art_filename, "rb") as cover_art:
        #     song.tag.images.set(3, cover_art.read(), "image/jpeg")
        # audiofile.tag.save()

        tag.title = title
        tag.artist = artist
        tag.album = album
        tag.genre = genre
        if type(lyric) == str:
            lyric = lyric.replace("\n","\r\n")
            tag.lyrics.set(lyric)

        if type(img) == bytes:
            tag.images.set(3, img, "image/jpeg", u"Cover")

        try:
            tag.save(version=(2, 3, 0))
        except UnicodeEncodeError as uee:
            print(f"UnicodeEncodeError: {uee}")

    def listen1_backup(self, path: str):
        if not os.path.exists(path):
            return False

        data = json.load(open(path, 'r', encoding='utf-8'))
        songs = OrderedDict()
        for player in data["playerlists"]:
            title = data[player]["info"]["title"]
            cho = input(f"\n`{title}`是否下载([y]/n)?  ")
            if cho == "" or cho == "y" or cho == "Y":
                songs[title] = data[player]["tracks"]
                print(f"`{title}`已添加到下载列表中")

        return songs

    def read_text(self, path: str):
        songs = OrderedDict()
        title = input("请给自己的歌单命名:")
        unworkes = []

        with open(path, 'r', encoding="utf-8") as f:
            names = f.readlines()
            names = [name.strip() for name in names]
            for name in names:
                url = f"https://cn.bing.com/search?q={name}+site:music.163.com"
                info = self.get(self.request_bing, url)
                if not url:
                    # print(f"\nrequest_bing return Error: {url}")
                    unworkes.append(name)
                    continue
                # print(f"request_bing url: {url}")

        return names

    def run(self, path: str):
        unworkes = []
        success = 0

        if not os.path.exists(self.songs_dirpath):
            os.makedirs(self.songs_dirpath)

        # listen1 和 text 两者读取歌单方式
        if path.endswith(".json"):
            songs = self.listen1_backup(path)
            self.pbar.total = sum([len(val) for val in songs.values()])
        elif path.endswith(".txt"):
            names = self.read_text(path)
            names = list(set(names))
            self.pbar.total = len(names)

        start = time.time()
        for name, tracks in songs.items():
            name = "".join([s for s in name if s not in " <>/\|:*?"])
            dirpath = os.path.join(self.songs_dirpath, name)
            if not os.path.exists(dirpath):
                os.makedirs(dirpath)

            for track in tracks:
                song_path = os.path.join(dirpath, track["title"]+".mp3")
                if os.path.exists(song_path) or track["source"] != "netease":
                    self.pbar.update()
                    continue

                id = re.search(r"id=\d+", track["source_url"]).group()
                mp3_url = f"http://music.163.com/song/media/outer/url?{id}.mp3"
                lyric_url = f"http://music.163.com/api/song/media?{id}"
                img_url = track["img_url"]
                # print(f"download mp3 file: {url}")
                if self.get(partial(self.download_mp3, path=song_path), mp3_url):
                    lyric = self.get(self.download_lyric, lyric_url)
                    img = self.get(self.get_image, img_url)

                    self.write_ID3_2(
                        song_path, img, track["title"], track["artist"], track["album"], name, lyric)
                    self.pbar.set_postfix_str(
                        f"`{name}`->`{track['title']}` 下载成功!")
                    success += 1
                else:
                    unworkes.append(f"`{name}`->`{track['title']}`")

                self.pbar.update()

        self.pbar.close()
        hua = time.time() - start
        print(f'总共耗时:{hua // 60}min, {hua % 60}s。')
        un = '\n'.join(unworkes)
        print(f"总共{sum([len(t) for t in songs.values()])}首歌曲,成功下载{success}首歌曲,未下载{len(unworkes)}首歌曲:\n{un}")


if __name__ == "__main__":
    MusicDownloader().run("listen1_backup.json")
